iT邦幫忙

2026 iThome 鐵人賽

DAY 29
0
Claude AI

資深工程師的 Claude Code 工作筆記系列 第 29 篇

Day 29:Mods,改 Claude Code 自己的那一層,去哪裡找、怎麼挑、怎麼寫

  • 分享至 

  • xImage
  •  

Day 17 的 hooks 在事件發生時跑一支 Claude Code 外面的 shell 腳本,回一個退出碼就結束。Day 21 的 plugin 是打包層。

2.1.287 加進來的 mods 介於兩者之間:它是一段 JavaScript 或 TypeScript,跑在 Claude Code 的行程裡面,能攔事件、改事件,還能畫介面:提示上方的一行狀態、逐字稿旁的面板、不等 Claude 回答就執行的指令。

這個機制 2026 年 10 月初才上線,API 還在變。

mod 是什麼

mod 是一種 plugin。plugin 的 hooks/hooks.json 裡如果有 modules 這個欄位,指向一個 JS 或 TS 檔,這個 plugin 就是 mod。它和其他 plugin 一樣從 marketplace 裝,同一個 plugin 也能同時帶 skill 與 MCP server。

那個檔案叫 hooks module,匯出 register(on),用 on('事件名', handler) 掛處理函式。handler 拿到三個參數:$ 是 mods API,e 是事件,next 交給下一個處理者,很像 middleware。處理一個事件有三種做法:

  • 看:記下發生了什麼,原樣交給 next。
  • 改:改掉事件內容再交出去,例如改寫 prompt、換模型。
  • 接手:自己回答、不呼叫 next,例如拒絕一個工具呼叫。

和其他擴充方式比,差在跑在哪、能不能畫畫面:

mod settings hook skill MCP server
是什麼 plugin 裡的函式,在 Claude Code 行程內被呼叫 事件發生時跑的 shell 指令、HTTP 請求或 prompt Claude 會讀的 SKILL.md 外部行程,提供工具給 Claude
能畫介面 能 不能 不能 不能
適合 要面板、提示上方的一行、自訂指令,或要改寫事件 已經有腳本,只想擋、放行或記錄 同樣的說明一直貼進對話 Claude 要碰外部系統

mod 和其他擴充方式跑在哪裡

它能碰到哪些地方

畫介面的位置叫 render site,最常用兩個:Pane 在全螢幕且夠寬的終端機裡是逐字稿旁的側欄,否則是提示上方一塊有框的區域;AbovePrompt 是提示輸入框正上方的一條帶狀區(band),所有 mod 共用。

有一處刻意碰不到:mod 不能改權限提示顯示的內容。settings.json 的 statusLine 則是另一套機制,跑 shell 腳本,不是 mod。

Claude Code 自己也有幾個功能是 mod,列在 /plugin 的 Installed 分頁 Built-in 區,例如接手 /diff 的 cc-plugin-diff。其中 You should know 預設關閉,它用側邊 agent 在長任務中幫你盯,在提示上方提醒你可能漏掉的事。用 /plugin enable cc-plugin-you-should-know@builtin 開啟,條件是第一方 session 且開著 telemetry。

裝別人的 mod 之前,先看它碰什麼

mod 跑在 Claude Code 裡,用的是你的權限,載入之後它可以:

  • 以你的身分讀寫檔案、啟動程式、連網。
  • 讀環境變數與設定檔,包括放在裡面的 API key。
  • 看到你送出的每個 prompt 與 Claude 的每個工具呼叫。
  • 改寫 prompt 或工具呼叫,或替你送出一個 prompt。
  • 在你被問之前就核准一個工具呼叫。
  • 用你的方案或 API key 呼叫模型。

而且 mod 沒有 sandbox。Day 26 的 sandbox 隔離的是 Claude 跑的 Bash 指令,mod 自己啟動的程式在 sandbox 外面。權限規則也一樣:Read(.env) 的 deny 管的是 Claude 的工具呼叫,mod 用自己的 $.fs.read 照樣讀得到。Day 26 說 deny 在每個 mode 都有效、settings hook 的 allow 也蓋不掉,mod 是例外:在沒有 managed settings、也不是 Team 或 Enterprise 方案的機器上,mod 可以核准一個 deny 規則會拒絕的呼叫。

所以要先看它碰什麼。mod 只能透過 $ 做事,靜態就看得出來,不用執行:

claude plugin validate ./some-mod

輸出裡的 hooks: 列出它處理哪些事件,calls: 列出它呼叫哪些 API。看到這幾種要多想一下:$.fs.write(寫檔)、$.process.run 與 $.process.spawn(啟動程式)、$.http.fetch(連網)、$.env.get 與 $.settings.read(可能讀到金鑰)、$.model.complete(花你的額度)、$.prompt.submit 與 $.session.send(替你送出)。calls: 只列方法、不列參數,出現 $.process.run 就等於什麼程式都能跑,沒有 $.http.fetch 也不代表不會連網。

裝 mod 之前要看的地方

有 managed settings 的機器、或登入 Team 與 Enterprise 時,會載入內建守衛,讓使用者的 mod 改不到組織管的 hooks、system prompt 與指令檔;組織也可以只允許自己管的 mod。互動 session 在還沒信任的目錄裡,回答信任提示之前不載入任何 mod。

要關掉有三種粒度:在 /plugin 停用單一個,內建的多數也能這樣關;用 --safe-mode 啟動,這次 session 全關,CLAUDE.md、skills、MCP 也會一起關;在 ~/.claude/settings.json 設 "disableAllHooks": true,永遠關,settings hooks 與自訂狀態列也會停,組織管的照跑。後兩種關不到內建 mod。

去哪裡找別人寫好的 mod

目前有三個地方:

  • 官方展示頁 claude.dev/mods:放三個官方範例、幾個內建 mod,以及一個社群區,目前只有一個,從 Anthropic 的社群 marketplace claude-community 安裝。三個範例的原始碼在 anthropics/claude-code-playground 的 claude-code/mods 目錄,官方標明是照原樣分享、不提供支援。
  • 社群目錄 mods.aidojo.si,原始資料在 GitHub 的 karanb192/awesome-claude-code-mods。它自動掃描公開 repo 並整理 validate 結果,可以依 Draws & remembers、Reads files、Writes or runs、Uses the network、In Anthropic directory 篩選。它自己寫明是獨立掃描,不是官方目錄,掃描也不代表行為安全。
  • plugin 的一般管道:mod 就是 plugin,所以 Anthropic 的目錄與 marketplace 也收含 mod 的 plugin。

幾個值得先看的

依存取範圍分兩組。先從第一組裝起:

只讀資料、只畫畫面

  1. token-weather(官方範例):每輪結束讀 context 用量,在提示上方畫一行「天氣預報」。不讀寫檔、不跑程式、不連網,適合第一個裝。
  2. flightdeck(社群,scasella/claude-flightdeck):用 /flightdeck 開即時面板,列出 context、費用、權限判斷結果與 subagent 狀態。它的工具呼叫 hook 只觀察、原樣放行,calls: 裡沒有讀寫檔、跑程式、連網的 API。

會跑程式或讀寫檔,裝之前先讀 README

  1. blast-radius(官方範例):攔下 rm -rf、git reset --hard、強制推送這類指令,開面板列出會動到的檔案或 commit,按 Proceed 才放行。為了算出影響,它會跑 git、bash,以及各框架查 migration 狀態的指令,後者會執行專案自己的程式碼。它比對的是命令文字,$(…)、別名都攔不到,README 寫明是安全網,不是權限系統,真要擋用權限規則。
  2. image-view(社群,jarrodwatts/claude-image-view):把貼進來的圖片縮圖畫在提示上方。會讀圖片暫存檔,必要時用 sips 轉檔並寫入暫存資料夾,不連網。Ghostty、kitty 顯示真實圖片,其他終端機改用色塊縮圖。
  3. filetree(社群,data-goblin/claude-code-filetree):側欄檔案樹,標出 git 狀態與 Claude 正在讀改的檔案。會跑 find、du、git,雙擊檔案預設用外部程式開啟;側欄需要全螢幕且至少 110 欄。

另外要小心會把資料送到第三方、把金鑰寫進明文檔、在背景起常駐伺服器、每輪額外花 token 的 mod。

試官方範例:clone 那個 repo,在 claude-code/mods 目錄跑 claude --plugin-dir ./token-weather,只在這次 session 載入。想長期用,在同一個目錄跑 claude plugin marketplace add ./,再 claude plugin install token-weather@claude-code-playground-mods。裝的時候若已開著 session,在那裡跑 /reload-plugins,否則下次啟動自動載入。

自己寫一個

有兩條路。一條是請 Claude 寫:Claude Code 內建一個 plugin-authoring skill,描述你要的 mod,它會寫在 ~/.claude/dev-mods/ 底下這個 session 的目錄裡。那是受保護的路徑,default 與 acceptEdits 模式下每個檔案都會先問你,第一次存檔時還會問要不要啟用熱重載。它只在那個 session 載入,過了 cleanupPeriodDays 會被清掉。

另一條是自己寫三個檔案,用 claude --plugin-dir 載入,存檔就熱重載。下面這個示意 mod 接 Day 28 的交接流程:每輪結束,在提示上方顯示 review.md 的指紋是否等於 staged diff 的指紋,需要 git 與 shasum。綠燈只代表指紋對得上,能不能提交仍由 Day 28 的守門腳本判斷。

.claude-plugin/plugin.json 寫 name、version、description,hooks/hooks.json 只有一行 { "modules": ["./register.js"] },程式放在 hooks/register.js:

// 指紋要在 shell 裡算:$.process.run 不經 shell,拿回來的 stdout 也是解碼過的文字
const FINGERPRINT =
  "git diff --cached --binary --no-ext-diff --no-textconv -- . ':!plan.md' ':!review.md' | shasum -a 256"
const EMPTY = 'e3b0c442'
let line = null

async function check($) {
  const cwd = await $.session.cwd()
  const top = await $.process.run(['git', 'rev-parse', '--show-toplevel'], { cwd, timeoutMs: 5000 })
  if (top.exitCode !== 0) return null
  // 一律到 repo 根目錄算,否則在子目錄只會算到那個子目錄的變更
  const root = top.stdout.trim()
  if (!(await $.fs.exists(root + '/review.md'))) return null
  // 和 Day 28 的守門腳本一樣:只看第一行 reviewed-fingerprint,值要剛好是 64 碼
  const header = (await $.fs.read(root + '/review.md')).match(/^reviewed-fingerprint: *([^\n]*)/m)
  const reviewed = header ? header[1] : ''
  if (!/^[0-9a-f]{64}$/.test(reviewed)) return 'handoff: review.md has no valid fingerprint'
  const run = await $.process.run(['sh', '-c', FINGERPRINT], { cwd: root, timeoutMs: 20000 })
  const staged = run.stdout.trim().split(/\s+/)[0]
  if (run.exitCode !== 0 || !/^[0-9a-f]{64}$/.test(staged)) return 'handoff: cannot hash staged diff'
  if (staged.startsWith(EMPTY)) return 'handoff: nothing staged'
  return staged === reviewed
    ? 'handoff: review matches ' + staged.slice(0, 8)
    : 'handoff: STALE review ' + reviewed.slice(0, 8) + ' vs staged ' + staged.slice(0, 8)
}

export function register(on) {
  on('session.start', async ($, e, next) => {
    await $.command.register({ name: 'handoff-check', description: 'Check review.md against the staged diff' })
    return next(e)
  })

  // 主回合結束後重算,subagent 的回合跳過
  on('turn.complete', async ($, e, next) => {
    const result = await next(e)
    if (e.agentId) return result
    line = await check($).catch(() => 'handoff: check failed')
    $.ui.invalidate('ui.render')
    return result
  })

  // 沒有畫面的時候(例如 claude -p),用指令拿同一個結果
  on('command.run', { command: 'handoff-check' }, async ($) => {
    const text = await check($).catch(() => 'handoff: check failed')
    return { text: text ?? 'handoff: no review.md here' }
  })

  // 問卷出現時讓開;其他 mod 畫的內容放在上面一起留著
  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
    if (e.props.hasSurvey || line === null) return next(e)
    const { Box, Text } = $.ui.resolve(e)
    const color = line.includes('matches') ? 'green' : 'yellow'
    return Box({ flexDirection: 'column', children: [await next(e), Text({ children: [line], color })] })
  })
}

寫的時候會碰到的幾件事:

  • 雜湊留在 shell 裡算。 $.process.run 不經 shell,stdout 是解碼過的文字且上限 4 MiB,拿到 JS 裡算雜湊會和位元組的不一樣。
  • 先找 repo 根目錄。 -- . 相對於目前目錄,在子目錄跑只會算到那個子目錄的變更,不會報錯。
  • $ 只能傳給同檔頂層的函式。 事件名要是字串字面值、不能動態 import(),好讓 validate 看得出每個呼叫,違反會被拒絕載入。
  • band 是共用的。 回傳自己的樹會蓋掉排在後面的 mod,所以把 await next(e) 一起放進 Box;有問卷時讓開。subagent 的回合也會觸發 turn.complete,用 e.agentId 跳過。
  • 模組變數熱重載會歸零。 示意 mod 的 line 歸零後,要等下一輪結束 band 才回來;要保留的放 $.state。
  • 一個 hook 自己的執行時間上限 10 秒,等 $ 的時間不算。
  • API 還在變,以載入時寫進 .claude-plugin/types/ 的型別檔為準。

驗證不需要登入:claude plugin validate . 列出呼叫;claude plugin test 跑自己寫的 tests/*.test.ts(從 claude-code/testing 引入),不需 session 與網路;指令型的功能在要檢查的 repo 裡跑 claude -p "/handoff-check" --plugin-dir 加上 mod 的絕對路徑。路徑寫錯不會報錯,指令會被當成一般 prompt。band 的實際外觀要開互動 session 才看得到。

分享自己的 mod

分享 mod 和 Day 21 分享 plugin 一樣,依對象選:

mod 的分享管道

  • 給幾個人:直接傳 plugin 目錄或 .zip。
  • 給團隊:放進自己的 marketplace,別人用 /plugin marketplace add 加入之後安裝。
  • 給整個組織:由管理者透過 managed settings 推給所有人。
  • 給所有人:把 marketplace 的 repo 公開,或在 claude.ai/directory/manage 提交到 Anthropic 的目錄,需要付費的 claude.ai 方案。想被社群目錄收錄,照它 repo 的說明發 PR 把 repo 加進 data/seeds.txt,加上 claude-code-mod topic 能讓掃描更快找到。

命名避開 claude- 開頭這類像官方的名稱,validate 會擋下。

今天的重點

  • mod 是一種 plugin:hooks/hooks.json 有 modules 欄位,指向在 Claude Code 行程內執行的 JS 或 TS。它能看、改、接手事件,還能畫介面,這是 settings hook、skill、MCP 都做不到的。
  • mod 用你的權限執行,沒有 sandbox,deny 規則也管不到它自己的檔案讀寫。裝之前跑 claude plugin validate,看 hooks: 與 calls:。
  • 找 mod:官方的 claude.dev/mods 與範例 repo,社群的 mods.aidojo.si 可以依存取範圍篩選,但它是獨立掃描,不是審核。
  • 先從只讀資料、只畫畫面的裝起,例如 token-weather。會跑程式的先讀 README;blast-radius 這類比對文字的守門是安全網,不是權限系統。
  • 自己寫:雜湊與管線留在 shell、先找 repo 根目錄、band 要留給別人、$ 只傳給頂層函式、跨重載的狀態放 $.state。validate、claude plugin test 與 claude -p "/指令" 都不用登入就能驗。

上一篇
Day 28:一個需求走完整條線,交接物決定下一步能不能開始
下一篇
Day 30:Claude Code 自己出問題時怎麼查,沒載入、沒匹配,還是被蓋掉
系列文
資深工程師的 Claude Code 工作筆記 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言